package com.googlecode.tidej.client.project;

import java.io.IOException;
import java.util.ArrayList;

import com.google.gwt.core.client.Duration;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.googlecode.tidej.client.Console;
import com.googlecode.tidej.client.TidejController;
import com.googlecode.tidej.client.project.ProjectMetaData.TargetData;
import com.googlecode.tidej.client.project.ProjectMetaData.TargetType;
import com.googlecode.tidej.client.util.DataSource;
import com.googlecode.tidej.client.util.HttpDataSource;
import com.googlecode.tidej.client.util.LocalStorage;
import com.googlecode.tidej.client.util.LocalStorageDataSource;
import com.googlecode.tidej.shared.jsdoc.JsDocParser;
import com.googlecode.tidej.shared.jsdoc.JsDocSerializer;
import com.googlecode.tidej.shared.model.Library;
import com.googlecode.tidej.shared.model.Model;

/**
 * Manages all IO aspects of a project.
 * 
 * @author haustein
 */
public class ProjectManager {
  static final String STORAGE_PREFIX = "projects/";
  String name;
  Model model;
  Library project;
  LocalStorageDataSource localDataSource;
  HttpDataSource remoteDataSource;
  String htmlOnly;
  ProjectMetaData metaData;
  TidejController controller;
  
  public static boolean exists(String name) {
    try {
      return LocalStorage.getItem(STORAGE_PREFIX + name + ".meta") != null;
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Inserts an empty script element close to the end of the page.
   */
  private static void insertScript(StringBuilder html) {
    String lower = html.toString().toLowerCase();
    int insertionPoint = lower.indexOf("</body");
    if (insertionPoint == -1) {
      insertionPoint = lower.indexOf("</html");
      if (insertionPoint == -1) {
        insertionPoint = html.length();
      }
    }
    html.insert(insertionPoint, "<script></script>");
  }

  /**
   * Inserts the given string as the script content in the script element
   * closest to the end of the file. If there is no script element yet,
   * one will be created by calling insertScript.
   */
  private static String swapScript(StringBuilder html, String swap) {
    String lower = html.toString().toLowerCase();
    int start = lower.lastIndexOf("<script");
    if (start == -1) {
      insertScript(html);
      return swapScript(html, swap);
    } 
    int contentStart = lower.indexOf('>', start) + 1;
    if (contentStart == 0) {
      html.insert(start + 7, ">");
      contentStart = start + 8;
    }
    int contentEnd = lower.indexOf("</script", contentStart);
    if (contentEnd == -1) {
      contentEnd = html.length();
      html.append("</script>");
    }
    String result = html.substring(contentStart, contentEnd);
    html.delete(contentStart, contentEnd);
    html.insert(contentStart, swap);
    return result;
  }
  
  public ProjectManager(TidejController controller, String name) {
    this.controller = controller;
    this.model = controller.getModel();
    this.name = name;
    this.project = new Library(name, false);
    localDataSource = new LocalStorageDataSource("projects/" + name + ".html");
  }

  public void create(ProjectMetaData metaData, DataSource fromSample,  final AsyncCallback<Void> callback) {
    if (exists(name)) {
      throw new RuntimeException("Project exists already");
    }
    this.metaData = metaData;
    metaData.save();
    if (fromSample == null) {
      fromSample = new MemoryDataSource("<html>\n" + 
          "<head><title>" + name + "</title></head>\n" +
          "<body><script></script>\n" + 
          "</html>");
    }
    
    Console.log("Creating project from: " + fromSample);
    fromSample.getContent(new AsyncCallback<String>() {
      @Override
      public void onFailure(Throwable caught) {
        callback.onFailure(caught);
      }
      @Override
      public void onSuccess(String result) {
        model.addLibrary(project);
        loadImpl(result, callback);
        saveCode();
      }
    });
  }


  public void open(final AsyncCallback<Void> callback) {
    if (!exists(name)) {
      RuntimeException exception = new RuntimeException(
          "Project '" + name + "' does not exist.");
      if (callback == null) {
        throw exception;
      } else {
        callback.onFailure(exception);
        return;
      }
    }

   DataSource source;
   final String url;
   metaData = ProjectMetaData.load(project.getName());
   TargetData data = metaData.getTargetData(TargetType.DEVELOPMENT);
   if (data.getUrl() != null && !data.getUrl().equals("")) {
     url = data.getUrl();
     remoteDataSource = new HttpDataSource(url);
     remoteDataSource.authenticate(data.getUser(), data.getPassword());
     source = remoteDataSource;
   } else {
     url = null;
     source = localDataSource;
   }
   
   Console.log("Loading project from: " + source);
   source.getContent(new AsyncCallback<String>() {
     @Override
     public void onFailure(Throwable caught) {
       callback.onFailure(caught);
       controller.setOnlineState(url, false);
     }
     @Override
     public void onSuccess(String result) {
       controller.setOnlineState(url, true);
       model.addLibrary(project);
       loadImpl(result, callback);
     }
   });
 }

  private void loadImpl(String html, final AsyncCallback<Void> callback) {
    Console.log("Loading single HTML file");
    StringBuilder sb = new StringBuilder(html == null ? "" : html);
    String script = swapScript(sb, 
        "\n\n// The script content will be inserted here.\n\n");
    
    this.htmlOnly = sb.toString();
    new JsDocParser(model, project, script).run();
    
    callback.onSuccess(null);
  }

  public String getHtmlOnly() {
    return htmlOnly;
  }
  
  public String getHtmlAndScript() {
    StringBuilder sb = new StringBuilder(htmlOnly);
    
    String script = JsDocSerializer.serializeProject(project);
    
    swapScript(sb, script);
    return sb.toString();
  }

  public void saveCode() {
    String htmlAndScript = getHtmlAndScript();
    if (remoteDataSource != null) {
      saveTo(remoteDataSource, htmlAndScript);
    }
    saveTo(localDataSource, htmlAndScript);
  }
  
  private void saveTo(DataSource dataSource, String htmlAndScript) {
    final double t0 = Duration.currentTimeMillis();
    Console.log("Saving project To: " + dataSource);
    dataSource.setContent(htmlAndScript, new AsyncCallback<Void>() {
      @Override
      public void onSuccess(Void result) {
        Console.log("Saved.. " + (Duration.currentTimeMillis()- t0) + "ms");
      }
      @Override
      public void onFailure(Throwable caught) {
        Window.alert("Saving failed: " + caught.getMessage());
      }
    });
  }
  
  public void deploy(DataSource dataSource) {
    saveTo(dataSource, getHtmlAndScript());
  }
  

  public void setHtmlOnly(String text) {
    this.htmlOnly = text;
  }

  public Library getProjectLibrary() {
    return project;
  }

  public ProjectMetaData getEntry() {
    return metaData;
  }

  public static Iterable<String> getProjectNames() {
    try {
      ArrayList<String> result = new ArrayList<String>();
      for (int i = 0; i < LocalStorage.length(); i++) {
        String name = LocalStorage.key(i);
        if (name.startsWith(STORAGE_PREFIX) && name.endsWith(".meta")) {
          result.add(name.substring(STORAGE_PREFIX.length(), name.length() - 5));
        }
      }
      return result;
    } catch(IOException e) {
      throw new RuntimeException(e);
    }
  }

  public void delete() {
    try {
      LocalStorage.removeItem(STORAGE_PREFIX + name + ".meta");
    } catch(Exception e) {
    }
    try {
      LocalStorage.removeItem(STORAGE_PREFIX + name + ".html");
    } catch(Exception e) {
    }
    // Make sure further actions on this will fail.
    name = null;
    model = null;
    project = null;
    metaData = null;
  }

  public String getName() {
    return name;
  }
}

